admin: Add an `unlock` command, and libostree API
authorColin Walters <walters@verbum.org>
Fri, 18 Mar 2016 19:32:58 +0000 (15:32 -0400)
committerColin Walters <walters@verbum.org>
Wed, 23 Mar 2016 15:09:09 +0000 (11:09 -0400)
I'm trying to improve the developer experience on OSTree-managed
systems, and I had an epiphany the other day - there's no reason we
have to be absolutely against mutating the current rootfs live.  The
key should be making it easy to rollback/reset to a known good state.

I see this command as useful for two related but distinct workflows:

 - `ostree admin unlock` will assume you're doing "development".  The
   semantics hare are that we mount an overlayfs on `/usr`, but the
   overlay data is in `/var/tmp`, and is thus discarded on reboot.
 - `ostree admin unlock --hotfix` first clones your current deployment,
   then creates an overlayfs over `/usr` persistent
   to this deployment.  Persistent in that now the initramfs switchroot
   tool knows how to mount it as well.  In this model, if you want
   to discard the hotfix, at the moment you roll back/reboot into
   the clone.

Note originally, I tried using `rofiles-fuse` over `/usr` for this,
but then everything immediately explodes because the default (at least
CentOS 7) SELinux policy denies tons of things (including `sshd_t`
access to `fusefs_t`).  Sigh.

So the switch to `overlayfs` came after experimentation.  It still
seems to have some issues...specifically `unix_chkpwd` is broken,
possibly because it's setuid?  Basically I can't ssh in anymore.

But I *can* `rpm -Uvh strace.rpm` which is handy.

NOTE: I haven't tested the hotfix path fully yet, specifically
the initramfs bits.

20 files changed:
Makefile-man.am
Makefile-ostree.am
buildutil/tap-test
man/ostree-admin-unlock.xml [new file with mode: 0644]
src/libostree/libostree.sym
src/libostree/ostree-deployment-private.h
src/libostree/ostree-deployment.c
src/libostree/ostree-deployment.h
src/libostree/ostree-sysroot-private.h
src/libostree/ostree-sysroot.c
src/libostree/ostree-sysroot.h
src/ostree/ot-admin-builtin-status.c
src/ostree/ot-admin-builtin-unlock.c [new file with mode: 0644]
src/ostree/ot-admin-builtin-upgrade.c
src/ostree/ot-admin-builtins.h
src/ostree/ot-builtin-admin.c
src/switchroot/ostree-mount-util.c
src/switchroot/ostree-mount-util.h
src/switchroot/ostree-prepare-root.c
src/switchroot/ostree-remount.c

index 615bf0f08d72ac8cdae45c7cc450993413d0398f..ce7e93cd9db9ac3a6f0bb93c693a35225106b60f 100644 (file)
 
 if ENABLE_MAN
 
-man1_files = ostree.1 ostree-admin-cleanup.1 ostree-admin-config-diff.1 ostree-admin-deploy.1 ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-os-init.1 ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1 ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1 ostree-commit.1 ostree-export.1 ostree-gpg-sign.1 ostree-config.1 ostree-diff.1 ostree-fsck.1 ostree-init.1 ostree-log.1 ostree-ls.1 ostree-prune.1 ostree-pull-local.1 ostree-pull.1 ostree-refs.1 ostree-remote.1 ostree-reset.1 ostree-rev-parse.1 ostree-show.1 ostree-summary.1 ostree-static-delta.1 ostree-trivial-httpd.1
+man1_files = ostree.1 ostree-admin-cleanup.1                           \
+ostree-admin-config-diff.1 ostree-admin-deploy.1                       \
+ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-os-init.1  \
+ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1  \
+ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin-unlock.1   \
+ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1                \
+ostree-commit.1 ostree-export.1 ostree-gpg-sign.1 ostree-config.1      \
+ostree-diff.1 ostree-fsck.1 ostree-init.1 ostree-log.1 ostree-ls.1     \
+ostree-prune.1 ostree-pull-local.1 ostree-pull.1 ostree-refs.1         \
+ostree-remote.1 ostree-reset.1 ostree-rev-parse.1 ostree-show.1                \
+ostree-summary.1 ostree-static-delta.1 ostree-trivial-httpd.1
 
 if BUILDOPT_FUSE
 man1_files += rofiles-fuse.1
index ff7e372b0ca0b69b7c3279224843bafbe95a8579..0ef5c4ee8a5bc7535d3e03db9cc900335c423bf6 100644 (file)
@@ -66,6 +66,7 @@ ostree_SOURCES += \
        src/ostree/ot-admin-builtin-status.c \
        src/ostree/ot-admin-builtin-switch.c \
        src/ostree/ot-admin-builtin-upgrade.c \
+       src/ostree/ot-admin-builtin-unlock.c \
        src/ostree/ot-admin-builtins.h \
        src/ostree/ot-admin-instutil-builtin-selinux-ensure-labeled.c \
        src/ostree/ot-admin-instutil-builtin-set-kargs.c \
index 38080bb333e97ce755f6f4b8556176085c96ab59..e7914541eaf0aa71ca43f98d257feb9d1edbae6f 100755 (executable)
@@ -12,7 +12,7 @@ tempdir=$(mktemp -d /var/tmp/tap-test.XXXXXX)
 touch ${tempdir}/.testtmp
 function cleanup () {
     if test -n "${TEST_SKIP_CLEANUP:-}"; then
-       echo "Skipping cleanup of ${test_tmpdir}"
+       echo "Skipping cleanup of ${tempdir}"
     else if test -f ${tempdir}/.test; then
        rm "${tempdir}" -rf
     fi
diff --git a/man/ostree-admin-unlock.xml b/man/ostree-admin-unlock.xml
new file mode 100644 (file)
index 0000000..ca02bbd
--- /dev/null
@@ -0,0 +1,88 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+    "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<!--
+Copyright 2016 Colin Walters <walters@verbum.org>
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the
+Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+Boston, MA 02111-1307, USA.
+-->
+
+<refentry id="ostree">
+
+    <refentryinfo>
+        <title>ostree admin unlock</title>
+        <productname>OSTree</productname>
+
+        <authorgroup>
+            <author>
+                <contrib>Developer</contrib>
+                <firstname>Colin</firstname>
+                <surname>Walters</surname>
+                <email>walters@verbum.org</email>
+            </author>
+        </authorgroup>
+    </refentryinfo>
+
+    <refmeta>
+        <refentrytitle>ostree admin unlock</refentrytitle>
+        <manvolnum>1</manvolnum>
+    </refmeta>
+
+    <refnamediv>
+        <refname>ostree-admin-unlock</refname>
+        <refpurpose>Prepare the current deployment for hotfix or development</refpurpose>
+    </refnamediv>
+
+    <refsynopsisdiv>
+            <cmdsynopsis>
+                <command>ostree admin unlock</command> <arg choice="opt" rep="repeat">OPTIONS</arg>
+            </cmdsynopsis>
+    </refsynopsisdiv>
+
+    <refsect1>
+        <title>Description</title>
+
+        <para>
+         Remove the read-only bind mount on <literal>/usr</literal>
+         and replace it with a writable overlay filesystem.  This
+         default invocation of "unlock" is intended for
+         development/testing purposes.  All changes in the overlay
+         are lost on reboot.  However, this command also supports
+         "hotfixes", see below.
+        </para>
+    </refsect1>
+
+    <refsect1>
+        <title>Options</title>
+
+        <variablelist>
+            <varlistentry>
+                <term><option>--hotfix</option></term>
+
+                <listitem><para>If this option is provided, the
+                current deployment will be cloned as a rollback
+                target.  This option is intended for things like
+                emergency security updates to userspace components
+                such as <literal>sshd</literal>.  The semantics here
+               differ from the default "development" unlock mode
+               in that reboots will retain any changes (which is what
+               you likely want for security hotfixes).
+                </para></listitem>
+            </varlistentry>
+        </variablelist>
+    </refsect1>
+</refentry>
index a85f0dbb53f17549279679ad9d9902e3e125b378..c78986e50647a49d337dc649b163b1a74115a11e 100644 (file)
@@ -318,4 +318,7 @@ global:
         ostree_repo_list_refs_ext;
         ostree_sysroot_init_osname;
         ostree_sysroot_load_if_changed;
+        ostree_sysroot_deployment_unlock;
+        ostree_deployment_get_unlocked;
+        ostree_deployment_unlocked_state_to_string;
 } LIBOSTREE_2016.3;
index b5ebb95735445a31088d78f82fbf7cc7455e3626..856a3987b070f609e198759a60a24c88e4f97653 100644 (file)
 
 G_BEGIN_DECLS
 
+struct _OstreeDeployment
+{
+  GObject       parent_instance;
+
+  int index;  /* Global offset */
+  char *osname;  /* osname */
+  char *csum;  /* OSTree checksum of tree */
+  int deployserial;  /* How many times this particular csum appears in deployment list */
+  char *bootcsum;  /* Checksum of kernel+initramfs */
+  int bootserial; /* An integer assigned to this tree per its ${bootcsum} */
+  OstreeBootconfigParser *bootconfig; /* Bootloader configuration */
+  GKeyFile *origin; /* How to construct an upgraded version of this tree */
+  OstreeDeploymentUnlockedState unlocked;  /* The unlocked state */
+};
+
 void _ostree_deployment_set_bootcsum (OstreeDeployment *self, const char *bootcsum);
 
 G_END_DECLS
index 3a80474e56d5db08c4d07a12e5a99ab7715c38b3..7b93e6cc886d5d25f1375e6d426de31ac7ba27b4 100644 (file)
 #include "ostree-deployment-private.h"
 #include "libglnx.h"
 
-struct _OstreeDeployment
-{
-  GObject       parent_instance;
-
-  int index;  /* Global offset */
-  char *osname;  /* osname */
-  char *csum;  /* OSTree checksum of tree */
-  int deployserial;  /* How many times this particular csum appears in deployment list */
-  char *bootcsum;  /* Checksum of kernel+initramfs */
-  int bootserial; /* An integer assigned to this tree per its ${bootcsum} */
-  OstreeBootconfigParser *bootconfig; /* Bootloader configuration */
-  GKeyFile *origin; /* How to construct an upgraded version of this tree */
-};
-
 typedef GObjectClass OstreeDeploymentClass;
 
 G_DEFINE_TYPE (OstreeDeployment, ostree_deployment, G_TYPE_OBJECT)
@@ -258,6 +244,7 @@ ostree_deployment_new (int    index,
   self->deployserial = deployserial;
   self->bootcsum = g_strdup (bootcsum);
   self->bootserial = bootserial;
+  self->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE;
   return self;
 }
 
@@ -279,3 +266,24 @@ ostree_deployment_get_origin_relpath (OstreeDeployment *self)
                           ostree_deployment_get_csum (self),
                           ostree_deployment_get_deployserial (self));
 }
+
+const char *
+ostree_deployment_unlocked_state_to_string (OstreeDeploymentUnlockedState state)
+{
+  switch (state)
+    {
+    case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+      return "none";
+    case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX:
+      return "hotfix";
+    case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
+      return "development";
+    }
+  g_assert_not_reached ();
+}
+
+OstreeDeploymentUnlockedState
+ostree_deployment_get_unlocked (OstreeDeployment *self)
+{
+  return self->unlocked;
+}
index a474b3502a6bde242e5275ef1084e0e4ff48f6ea..bde0cf3726c0def25b53e593d224f415aa5e8906 100644 (file)
@@ -78,4 +78,16 @@ OstreeDeployment *ostree_deployment_clone (OstreeDeployment *self);
 _OSTREE_PUBLIC
 char *ostree_deployment_get_origin_relpath (OstreeDeployment *self);
 
+typedef enum {
+  OSTREE_DEPLOYMENT_UNLOCKED_NONE,
+  OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT,
+  OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX
+} OstreeDeploymentUnlockedState;
+
+_OSTREE_PUBLIC
+const char *ostree_deployment_unlocked_state_to_string (OstreeDeploymentUnlockedState state);
+
+_OSTREE_PUBLIC
+OstreeDeploymentUnlockedState ostree_deployment_get_unlocked (OstreeDeployment *self);
+
 G_END_DECLS
index 229893d2abb74c4381e81c19a24667132627d077..d210a36f0d2d66ed3400ccd8489252cd1dbfc331 100644 (file)
@@ -58,6 +58,9 @@ struct OstreeSysroot {
 };
 
 #define OSTREE_SYSROOT_LOCKFILE "ostree/lock"
+/* We keep some transient state in /run */
+#define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR "/run/ostree/deployment-state/"
+#define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT "unlocked-development"
 
 gboolean
 _ostree_sysroot_read_boot_loader_configs (OstreeSysroot *self,
index aa08838add2925267ea27af0cff9b2460bc7017d..6ee3dff9a80e413a9fcc7c80ce33bf074e43c8bd 100644 (file)
@@ -24,6 +24,7 @@
 
 #include "ostree-core-private.h"
 #include "ostree-sysroot-private.h"
+#include "ostree-deployment-private.h"
 #include "ostree-bootloader-uboot.h"
 #include "ostree-bootloader-syslinux.h"
 #include "ostree-bootloader-grub2.h"
@@ -646,6 +647,16 @@ parse_bootlink (const char    *bootlink,
   return ret;
 }
 
+static char *
+get_unlocked_development_path (OstreeDeployment *deployment)
+{
+  return g_strdup_printf ("%s%s.%d/%s",
+                          _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR,
+                          ostree_deployment_get_csum (deployment),
+                          ostree_deployment_get_deployserial (deployment),
+                          _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT);
+}
+
 static gboolean
 parse_deployment (OstreeSysroot       *self,
                   const char          *boot_link,
@@ -667,6 +678,8 @@ parse_deployment (OstreeSysroot       *self,
   g_autofree char *treebootserial_target = NULL;
   g_autofree char *deploy_dir = NULL;
   GKeyFile *origin = NULL;
+  g_autofree char *unlocked_development_path = NULL;
+  struct stat stbuf;
 
   if (!ensure_sysroot_fd (self, error))
     goto out;
@@ -704,6 +717,24 @@ parse_deployment (OstreeSysroot       *self,
   if (origin)
     ostree_deployment_set_origin (ret_deployment, origin);
 
+  ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE;
+  unlocked_development_path = get_unlocked_development_path (ret_deployment);
+  if (lstat (unlocked_development_path, &stbuf) == 0)
+    ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT;
+  else
+    {
+      g_autofree char *existing_unlocked_state =
+        g_key_file_get_string (origin, "origin", "unlocked", NULL);
+
+      if (g_strcmp0 (existing_unlocked_state, "hotfix") == 0)
+        {
+          ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX;
+        }
+      /* TODO: warn on unknown unlock types? */
+    }
+
+  g_debug ("Deployment %s.%d unlocked=%d", treecsum, deployserial, ret_deployment->unlocked);
+
   ret = TRUE;
   if (out_deployment)
     *out_deployment = g_steal_pointer (&ret_deployment);
@@ -1481,6 +1512,10 @@ ostree_sysroot_init_osname (OstreeSysroot       *self,
  *
  * If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN is
  * specified, then all current deployments will be kept.
+ *
+ * If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT is
+ * specified, then instead of prepending, the new deployment will be
+ * added right after the booted or merge deployment, instead of first.
  */
 gboolean
 ostree_sysroot_simple_write_deployment (OstreeSysroot      *sysroot,
@@ -1497,6 +1532,8 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot      *sysroot,
   g_autoptr(GPtrArray) deployments = NULL;
   g_autoptr(GPtrArray) new_deployments = g_ptr_array_new_with_free_func (g_object_unref);
   gboolean retain = (flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN) > 0;
+  const gboolean make_default = !((flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT) > 0);
+  gboolean added_new = FALSE;
 
   deployments = ostree_sysroot_get_deployments (sysroot);
   booted_deployment = ostree_sysroot_get_booted_deployment (sysroot);
@@ -1504,23 +1541,44 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot      *sysroot,
   if (osname == NULL && booted_deployment)
     osname = ostree_deployment_get_osname (booted_deployment);
 
-  g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
+  if (make_default)
+    {
+      g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
+      added_new = TRUE;
+    }
 
   for (i = 0; i < deployments->len; i++)
     {
       OstreeDeployment *deployment = deployments->pdata[i];
+      const gboolean is_merge_or_booted = 
+        ostree_deployment_equal (deployment, booted_deployment) ||
+        ostree_deployment_equal (deployment, merge_deployment);
       
       /* Keep deployments with different osnames, as well as the
        * booted and merge deployments
        */
       if (retain ||
-          (osname != NULL &&
-           strcmp (ostree_deployment_get_osname (deployment), osname) != 0) ||
-          ostree_deployment_equal (deployment, booted_deployment) ||
-          ostree_deployment_equal (deployment, merge_deployment))
+          (osname != NULL && strcmp (ostree_deployment_get_osname (deployment), osname) != 0) ||
+          is_merge_or_booted)
         {
           g_ptr_array_add (new_deployments, g_object_ref (deployment));
         }
+
+      if (!added_new)
+        {
+          g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
+          added_new = TRUE;
+        }
+    }
+
+  /* In this non-default case , an improvement in the future would be
+   * to put the new deployment right after the current default in the
+   * order.
+   */
+  if (!added_new)
+    {
+      g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
+      added_new = TRUE;
     }
 
   if (!ostree_sysroot_write_deployments (sysroot, new_deployments, cancellable, error))
@@ -1533,3 +1591,263 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot      *sysroot,
  out:
   return ret;
 }
+
+static gboolean
+clone_deployment (OstreeSysroot  *sysroot,
+                  OstreeDeployment *target_deployment,
+                  OstreeDeployment *merge_deployment,
+                  GCancellable *cancellable,
+                  GError **error)
+{
+  gboolean ret = FALSE;
+  __attribute__((cleanup(_ostree_kernel_args_cleanup))) OstreeKernelArgs *kargs = NULL;
+  glnx_unref_object OstreeDeployment *new_deployment = NULL;
+
+  /* Ensure we have a clean slate */
+  if (!ostree_sysroot_prepare_cleanup (sysroot, cancellable, error))
+    {
+      g_prefix_error (error, "Performing initial cleanup: ");
+      goto out;
+    }
+
+  kargs = _ostree_kernel_args_new ();
+
+  { OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (merge_deployment);
+    g_auto(GStrv) previous_args = g_strsplit (ostree_bootconfig_parser_get (bootconfig, "options"), " ", -1);
+    
+    _ostree_kernel_args_append_argv (kargs, previous_args);
+  }
+
+  {
+    g_auto(GStrv) kargs_strv = _ostree_kernel_args_to_strv (kargs);
+
+    if (!ostree_sysroot_deploy_tree (sysroot,
+                                     ostree_deployment_get_osname (target_deployment),
+                                     ostree_deployment_get_csum (target_deployment),
+                                     ostree_deployment_get_origin (target_deployment),
+                                     merge_deployment,
+                                     kargs_strv,
+                                     &new_deployment,
+                                     cancellable, error))
+      goto out;
+  }
+
+  /* Hotfixes push the deployment as rollback target, so it shouldn't
+   * be the default.
+   */
+  if (!ostree_sysroot_simple_write_deployment (sysroot, ostree_deployment_get_osname (target_deployment),
+                                               new_deployment, merge_deployment,
+                                               OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT,
+                                               cancellable, error))
+    goto out;
+  
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+/**
+ * ostree_sysroot_deployment_unlock:
+ * @self: Sysroot
+ * @deployment: Deployment
+ * @unlocked_state: Transition to this unlocked state
+ * @cancellable: Cancellable
+ * @error: Error
+ *
+ * Configure the target deployment @deployment such that it
+ * is writable.  There are multiple modes, essentially differing
+ * in whether or not any changes persist across reboot.
+ *
+ * The `OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX` state is persistent
+ * across reboots.
+ */
+gboolean
+ostree_sysroot_deployment_unlock (OstreeSysroot     *self,
+                                  OstreeDeployment  *deployment,
+                                  OstreeDeploymentUnlockedState unlocked_state,
+                                  GCancellable      *cancellable,
+                                  GError           **error)
+{
+  gboolean ret = FALSE;
+  OstreeDeploymentUnlockedState current_unlocked =
+    ostree_deployment_get_unlocked (deployment); 
+  glnx_unref_object OstreeDeployment *deployment_clone =
+    ostree_deployment_clone (deployment);
+  glnx_unref_object OstreeDeployment *merge_deployment = NULL;
+  GKeyFile *origin_clone = ostree_deployment_get_origin (deployment_clone);
+  const char hotfix_ovl_options[] = "lowerdir=usr,upperdir=.usr-ovl-upper,workdir=.usr-ovl-work";
+  const char *ovl_options = NULL;
+  g_autofree char *deployment_path = NULL;
+  glnx_fd_close int deployment_dfd = -1;
+  pid_t mount_child;
+
+  /* This function cannot re-lock */
+  g_return_val_if_fail (unlocked_state != OSTREE_DEPLOYMENT_UNLOCKED_NONE, FALSE);
+
+  if (current_unlocked != OSTREE_DEPLOYMENT_UNLOCKED_NONE)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Deployment is already in unlocked state: %s",
+                   ostree_deployment_unlocked_state_to_string (current_unlocked));
+      goto out;
+    }
+
+  merge_deployment = ostree_sysroot_get_merge_deployment (self, ostree_deployment_get_osname (deployment));
+  if (!merge_deployment)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "No previous deployment to duplicate");
+      goto out;
+    }
+
+  /* For hotfixes, we push a rollback target */
+  if (unlocked_state == OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX)
+    {
+      if (!clone_deployment (self, deployment, merge_deployment, cancellable, error))
+        goto out;
+    }
+
+  /* Crack it open */
+  if (!ostree_sysroot_deployment_set_mutable (self, deployment, TRUE,
+                                              cancellable, error))
+    goto out;
+
+  deployment_path = ostree_sysroot_get_deployment_dirpath (self, deployment);
+
+  if (!glnx_opendirat (self->sysroot_fd, deployment_path, TRUE, &deployment_dfd, error))
+    goto out;
+
+  switch (unlocked_state)
+    {
+    case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+      g_assert_not_reached ();
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX:
+      {
+        /* Create the overlayfs directories in the deployment root
+         * directly for hotfixes.  The ostree-prepare-root.c helper
+         * is also set up to detect and mount these.
+         */
+        if (!glnx_shutil_mkdir_p_at (deployment_dfd, ".usr-ovl-upper", 0755, cancellable, error))
+          goto out;
+        if (!glnx_shutil_mkdir_p_at (deployment_dfd, ".usr-ovl-work", 0755, cancellable, error))
+          goto out;
+        ovl_options = hotfix_ovl_options;
+      }
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
+      {
+        /* We're just doing transient development/hacking?  Okay,
+         * stick the overlayfs bits in /var/tmp.
+         */
+        char *development_ovldir = strdupa ("/var/tmp/ostree-unlock-ovl.XXXXXX");
+        const char *development_ovl_upper;
+        const char *development_ovl_work;
+
+        if (!glnx_mkdtempat (AT_FDCWD, development_ovldir, 0700, error))
+          goto out;
+
+        development_ovl_upper = glnx_strjoina (development_ovldir, "/upper");
+        if (!glnx_shutil_mkdir_p_at (AT_FDCWD, development_ovl_upper, 0755, cancellable, error))
+          goto out;
+        development_ovl_work = glnx_strjoina (development_ovldir, "/work");
+        if (!glnx_shutil_mkdir_p_at (AT_FDCWD, development_ovl_work, 0755, cancellable, error))
+          goto out;
+        ovl_options = glnx_strjoina ("lowerdir=usr,upperdir=", development_ovl_upper,
+                                     ",workdir=", development_ovl_work);
+      }
+    }
+
+  g_assert (ovl_options != NULL);
+
+  /* Here we run `mount()` in a fork()ed child because we need to use
+   * `chdir()` in order to have the mount path options to overlayfs not
+   * look ugly.
+   *
+   * We can't `chdir()` inside a shared library since there may be
+   * threads, etc.
+   */
+  {
+    /* Make a copy of the fd that's *not* FD_CLOEXEC so that we pass
+     * it to the child.
+     */
+    glnx_fd_close int child_deployment_dfd = dup (deployment_dfd);
+
+    if (child_deployment_dfd < 0)
+      {
+        glnx_set_error_from_errno (error);
+        goto out;
+      }
+
+    mount_child = fork ();
+    if (mount_child < 0)
+      {
+        glnx_set_prefix_error_from_errno (error, "%s", "fork");
+        goto out;
+      }
+    else if (mount_child == 0)
+      {
+        /* Child process.  Do NOT use any GLib API here. */
+        if (fchdir (child_deployment_dfd) < 0)
+          exit (EXIT_FAILURE);
+        (void) close (child_deployment_dfd);
+        if (mount ("overlay", "/usr", "overlay", 0, ovl_options) < 0)
+          exit (EXIT_FAILURE);
+        exit (EXIT_SUCCESS);
+      }
+    else
+      {
+        /* Parent */
+        int estatus;
+
+        if (TEMP_FAILURE_RETRY (waitpid (mount_child, &estatus, 0)) < 0)
+          {
+            glnx_set_prefix_error_from_errno (error, "%s", "waitpid() on mount helper");
+            goto out;
+          }
+        if (!g_spawn_check_exit_status (estatus, error))
+          {
+            g_prefix_error (error, "overlayfs mount helper: "); 
+            goto out;
+          }
+      }
+  }
+
+  /* Now, write out the flag saying what we did */
+  switch (unlocked_state)
+    {
+    case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+      g_assert_not_reached ();
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX:
+      g_key_file_set_string (origin_clone, "origin", "unlocked",
+                             ostree_deployment_unlocked_state_to_string (unlocked_state));
+      if (!ostree_sysroot_write_origin_file (self, deployment, origin_clone,
+                                             cancellable, error))
+        goto out;
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
+      {
+        g_autofree char *devpath = get_unlocked_development_path (deployment);
+        g_autofree char *devpath_parent = dirname (g_strdup (devpath));
+
+        if (!glnx_shutil_mkdir_p_at (AT_FDCWD, devpath_parent, 0755, cancellable, error))
+          goto out;
+        
+        if (!g_file_set_contents (devpath, "", 0, error))
+          goto out;
+      }
+    }
+
+  /* For hotfixes we already pushed a rollback which will bump the
+   * mtime, but we need to bump it again so that clients get the state
+   * change for this deployment.  For development we need to do this
+   * regardless.
+   */
+  if (!_ostree_sysroot_bump_mtime (self, error))
+    goto out;
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
index 077862aa97281f128982a1f3584c263380e0e405..1e60ddbe452360bf94d96cf0405fe992643f3135 100644 (file)
@@ -163,6 +163,13 @@ gboolean ostree_sysroot_deployment_set_mutable (OstreeSysroot     *self,
                                                 GCancellable      *cancellable,
                                                 GError           **error);
 
+_OSTREE_PUBLIC
+gboolean ostree_sysroot_deployment_unlock (OstreeSysroot     *self,
+                                           OstreeDeployment  *deployment,
+                                           OstreeDeploymentUnlockedState unlocked_state,
+                                           GCancellable      *cancellable,
+                                           GError           **error);
+
 _OSTREE_PUBLIC
 OstreeDeployment *ostree_sysroot_get_merge_deployment (OstreeSysroot     *self,
                                                        const char        *osname);
@@ -174,7 +181,8 @@ GKeyFile *ostree_sysroot_origin_new_from_refspec (OstreeSysroot      *self,
 
 typedef enum {
   OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NONE = 0,
-  OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN = (1 << 0)
+  OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN = (1 << 0),
+  OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT = (1 << 1)
 } OstreeSysrootSimpleWriteDeploymentFlags;
 
 _OSTREE_PUBLIC
index cd275cc6987d48339d32a2e632d4299148724a02..df4d074563ed70a6e8b1ecb4dc6e51133f2b75f4 100644 (file)
@@ -89,6 +89,9 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro
   glnx_unref_object OstreeRepo *repo = NULL;
   OstreeDeployment *booted_deployment = NULL;
   g_autoptr(GPtrArray) deployments = NULL;
+  const int is_tty = isatty (1);
+  const char *red_bold_prefix = is_tty ? "\x1b[31m\x1b[1m" : "";
+  const char *red_bold_suffix = is_tty ? "\x1b[22m\x1b[0m" : "";
   guint i;
 
   context = g_option_context_new ("List deployments");
@@ -118,12 +121,15 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro
           OstreeDeployment *deployment = deployments->pdata[i];
           GKeyFile *origin;
           const char *ref = ostree_deployment_get_csum (deployment);
+          OstreeDeploymentUnlockedState unlocked = ostree_deployment_get_unlocked (deployment);
           g_autofree char *version = version_of_commit (repo, ref);
           glnx_unref_object OstreeGpgVerifyResult *result = NULL;
           GString *output_buffer;
           guint jj, n_signatures;
           GError *local_error = NULL;
 
+          origin = ostree_deployment_get_origin (deployment);
+
           g_print ("%c %s %s.%d\n",
                    deployment == booted_deployment ? '*' : ' ',
                    ostree_deployment_get_osname (deployment),
@@ -131,7 +137,15 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro
                    ostree_deployment_get_deployserial (deployment));
           if (version)
             g_print ("    Version: %s\n", version);
-          origin = ostree_deployment_get_origin (deployment);
+          switch (unlocked)
+            {
+            case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+              break;
+            default:
+              g_print ("    %sUnlocked: %s%s\n", red_bold_prefix,
+                       ostree_deployment_unlocked_state_to_string (unlocked),
+                       red_bold_suffix);
+            }
           if (!origin)
             g_print ("    origin: none\n");
           else
diff --git a/src/ostree/ot-admin-builtin-unlock.c b/src/ostree/ot-admin-builtin-unlock.c
new file mode 100644 (file)
index 0000000..9d26532
--- /dev/null
@@ -0,0 +1,104 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2016 Colin Walters <walters@verbum.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "ot-main.h"
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "ostree.h"
+#include "otutil.h"
+
+#include "../libostree/ostree-kernel-args.h"
+
+#include <glib/gi18n.h>
+#include <err.h>
+
+static gboolean opt_hotfix;
+
+static GOptionEntry options[] = {
+  { "hotfix", 0, 0, G_OPTION_ARG_NONE, &opt_hotfix, "Keep the current deployment as default", NULL },
+  { NULL }
+};
+
+gboolean
+ot_admin_builtin_unlock (int argc, char **argv, GCancellable *cancellable, GError **error)
+{
+  gboolean ret = FALSE;
+  GOptionContext *context;
+  glnx_unref_object OstreeSysroot *sysroot = NULL;
+  glnx_unref_object OstreeRepo *repo = NULL;
+  g_autoptr(GPtrArray) new_deployments = NULL;
+  glnx_unref_object OstreeDeployment *merge_deployment = NULL;
+  OstreeDeployment *booted_deployment = NULL;
+  OstreeDeploymentUnlockedState target_state;
+
+  context = g_option_context_new ("Make the current deployment mutable (as a hotfix or development)");
+
+  if (!ostree_admin_option_context_parse (context, options, &argc, &argv,
+                                          OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER,
+                                          &sysroot, cancellable, error))
+    goto out;
+  
+  if (argc > 1)
+    {
+      ot_util_usage_error (context, "This command takes no extra arguments", error);
+      goto out;
+    }
+
+  if (!ostree_sysroot_load (sysroot, cancellable, error))
+    goto out;
+
+  booted_deployment = ostree_sysroot_get_booted_deployment (sysroot);
+  if (!booted_deployment)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "Not currently booted into an OSTree system");
+      goto out;
+    }
+
+  target_state = opt_hotfix ? OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX : OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT;
+
+  if (!ostree_sysroot_deployment_unlock (sysroot, booted_deployment,
+                                         target_state, cancellable, error))
+    goto out;
+  
+  switch (target_state)
+    {
+    case OSTREE_DEPLOYMENT_UNLOCKED_NONE:
+      g_assert_not_reached ();
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX:
+      g_print ("Hotfix mode enabled.  A writable overlayfs is now mounted on /usr\n"
+               "for this booted deployment.  A non-hotfixed clone has been created\n"
+               "as the non-default rollback target.\n");
+      break;
+    case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
+      g_print ("Development mode enabled.  A writable overlayfs is now mounted on /usr.\n"
+               "All changes there will be discarded on reboot.\n");
+      break;
+    }
+
+  ret = TRUE;
+ out:
+  if (context)
+    g_option_context_free (context);
+  return ret;
+}
index 5d4796ac5853d893a8c1db145eba7cc01916d585..b3b531c0be47b23dc63035e8cf5fd578e1e06e1e 100644 (file)
@@ -97,6 +97,9 @@ ot_admin_builtin_upgrade (int argc, char **argv, GCancellable *cancellable, GErr
                                                   "override-commit", NULL);
         }
 
+      /* Should we consider requiring --discard-hotfix here? */
+      origin_changed |= g_key_file_remove_key (origin, "origin", "unlocked", NULL);
+
       if (origin_changed)
         {
           /* XXX GCancellable parameter is not used. */
index 1a3c12648dcfffb66c9b5794e69be0e8f9c37411..f9425119e3ce29efe742fef99eeafc56a1158516 100644 (file)
@@ -34,6 +34,7 @@ gboolean ot_admin_builtin_init_fs (int argc, char **argv, GCancellable *cancella
 gboolean ot_admin_builtin_undeploy (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_deploy (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_cleanup (int argc, char **argv, GCancellable *cancellable, GError **error);
+gboolean ot_admin_builtin_unlock (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_set_origin (int argc, char **argv, GCancellable *cancellable, GError **error);
 gboolean ot_admin_builtin_diff (int argc, char **argv, GCancellable *cancellable, GError **error);
index a4cb0dde330d9d915514ca32596d1663d151ea51..8b866170a344ccca49105a09f7d987194d02371a 100644 (file)
@@ -47,6 +47,7 @@ static OstreeAdminCommand admin_subcommands[] = {
   { "status", ot_admin_builtin_status },
   { "switch", ot_admin_builtin_switch },
   { "undeploy", ot_admin_builtin_undeploy },
+  { "unlock", ot_admin_builtin_unlock }, 
   { "upgrade", ot_admin_builtin_upgrade },
   { NULL, NULL }
 };
index c6df559cd71061627486713c8787647e1ff60d9f..daec66c5762da746b99d7d56375f76eb6f26276b 100644 (file)
 #include <errno.h>
 #include <string.h>
 #include <stdio.h>
+#include <sys/mount.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <sys/statvfs.h>
 
 #include "ostree-mount-util.h"
 
@@ -48,3 +52,18 @@ perrorv (const char *format, ...)
 
   return 0;
 }
+
+int
+path_is_on_readonly_fs (char *path)
+{
+  struct statvfs stvfsbuf;
+
+  if (statvfs (path, &stvfsbuf) == -1)
+    {
+      perrorv ("statvfs(%s): ", path);
+      exit (EXIT_FAILURE);
+    }
+
+  return (stvfsbuf.f_flag & ST_RDONLY) != 0;
+}
+
index 63e90c67a93f68f4c586cc524f63eb7df179261b..475b2cab90078d83f503889918e9b4d8e8950dee 100644 (file)
@@ -22,3 +22,5 @@
 #pragma once
 
 int perrorv (const char *format, ...) __attribute__ ((format (printf, 1, 2)));
+
+int path_is_on_readonly_fs (char *path);
index 3de137bb365b82bbf4f30ed3c7a5763d00d33f39..375867b1eabfefb066c10c52fc2a63d4515aa312 100644 (file)
@@ -111,7 +111,6 @@ touch_run_ostree (void)
 int
 main(int argc, char *argv[])
 {
-  const char *readonly_bind_mounts[] = { "/usr", NULL };
   const char *root_mountpoint = NULL;
   char *ostree_target = NULL;
   char *deploy_path = NULL;
@@ -119,7 +118,7 @@ main(int argc, char *argv[])
   char destpath[PATH_MAX];
   char newroot[PATH_MAX];
   struct stat stbuf;
-  int i;
+  int orig_cwd_dfd;
 
   if (argc < 2)
     {
@@ -211,22 +210,71 @@ main(int argc, char *argv[])
         }
     }
 
-  /* Set up any read-only bind mounts (notably /usr) */
-  for (i = 0; readonly_bind_mounts[i] != NULL; i++)
+  /* Here we do a dance to chdir to the newroot so that we can have
+   * the potential overlayfs mount points not look ugly.  However...I
+   * think we could do this a lot earlier and make all of the mounts
+   * here just be relative.
+   */
+  orig_cwd_dfd = openat (AT_FDCWD, ".", O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+  if (orig_cwd_dfd < 0)
+    {
+      perrorv ("failed to open .");
+      exit (EXIT_FAILURE);
+    }
+
+  if (chdir (newroot) < 0)
     {
-      snprintf (destpath, sizeof(destpath), "%s%s", newroot, readonly_bind_mounts[i]);
-      if (mount (destpath, destpath, NULL, MS_BIND, NULL) < 0)
+      perrorv ("failed to chdir to newroot");
+      exit (EXIT_FAILURE);
+    }
+
+  /* Do we have a persistent overlayfs for /usr?  If so, mount it now. */
+  if (lstat (".usr-ovl-work", &stbuf) == 0)
+    {
+      const char usr_ovl_options[] = "lowerdir=usr,upperdir=.usr-ovl-upper,workdir=.usr-ovl-work";
+
+      /* Except overlayfs barfs if we try to mount it on a read-only
+       * filesystem.  For this use case I think admins are going to be
+       * okay if we remount the rootfs here, rather than waiting until
+       * later boot and `systemd-remount-fs.service`.
+       */
+      if (path_is_on_readonly_fs ("."))
        {
-         perrorv ("failed to bind mount (class:readonly) %s", destpath);
+         if (mount (".", ".", NULL, MS_REMOUNT | MS_SILENT, NULL) < 0)
+           {
+             perrorv ("Failed to remount rootfs writable (for overlayfs)");
+             exit (EXIT_FAILURE);
+           }
+       }
+      
+      if (mount ("overlay", "usr", "overlay", 0, usr_ovl_options) < 0)
+       {
+         perrorv ("failed to mount /usr overlayfs");
+         exit (EXIT_FAILURE);
+       }
+    }
+  else
+    {
+      /* Otherwise, a read-only bind mount for /usr */
+      if (mount ("usr", "usr", NULL, MS_BIND, NULL) < 0)
+       {
+         perrorv ("failed to bind mount (class:readonly) /usr");
          exit (EXIT_FAILURE);
        }
-      if (mount (destpath, destpath, NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL) < 0)
+      if (mount ("usr", "usr", NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL) < 0)
        {
-         perrorv ("failed to bind mount (class:readonly) %s", destpath);
+         perrorv ("failed to bind mount (class:readonly) /usr");
          exit (EXIT_FAILURE);
        }
     }
 
+  if (fchdir (orig_cwd_dfd) < 0)
+    {
+      perrorv ("failed to chdir to orig root");
+      exit (EXIT_FAILURE);
+    }
+  (void) close (orig_cwd_dfd);
+
   touch_run_ostree ();
 
   /* Move physical root to $deployment/sysroot */
index b8d3a963c671f3f0cd1dd2c059977a0570b85729..aecaf9a88e0c7d18aa99c23bcfedbb7536311d23 100644 (file)
 
 #include "ostree-mount-util.h"
 
-static int
-path_is_on_readonly_fs (char *path)
-{
-  struct statvfs stvfsbuf;
-
-  if (statvfs (path, &stvfsbuf) == -1)
-    {
-      perrorv ("statvfs(%s): ", path);
-      exit (EXIT_FAILURE);
-    }
-
-  return (stvfsbuf.f_flag & ST_RDONLY) != 0;
-}
-
 /* Having a writeable /var is necessary for full system functioning.
  * If /var isn't writeable, we mount tmpfs over it. While this is
  * somewhat outside of ostree's scope, having all /var twiddling